﻿using System;
using System.Collections;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using Quail.Helpers;

namespace Quail
{
    /// <summary>
    /// Default interface for all test shells
    /// </summary>
    public abstract class TestShell<TBrowser, TPage> : ITestShell
        where TBrowser : IWebBrowser
        where TPage : IWebPage
    {
        private static readonly Hashtable ValueHash = new Hashtable();
        private static long _testsInProgress;
        private TestSession _testSession;
        private bool _disposed;

        protected readonly string AssemblyName, ClassName;

        /// <summary>
        /// Initializes a new instance of the SAJTestShell class.
        /// </summary>
        /// <param name="testname">Name of the test</param>
        /// <param name="owninginstance">The object that is creating the SAJTestShell (for logging purposes)</param>
        protected TestShell(string testname, object owninginstance)
        {
            var t = owninginstance.GetType();
            AssemblyName = t.Module.Assembly.GetName().Name;
            ClassName = t.ToString();
            TestName = testname;
            Interlocked.Increment(ref _testsInProgress);
        }

        /// <summary>
        /// Gets or sets a custom SharedParallelState object for the test shell
        /// </summary>
        public object SharedParallelState
        {
            get; set;
        }

        /// <summary>
        /// Gets or sets a custom InstanceState object for the test shell
        /// </summary>
        public object InstanceState
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the page delay in milliseconds
        /// </summary>
        public int PageDelay
        {
            get; set;
        }

        /// <summary>
        /// Gets the current test name
        /// </summary>
        public string TestName
        {
            get; private set;
        }

        /// <summary>
        /// Gets the current test session
        /// </summary>
        public TestSession TestSession
        {
            get
            {
                if (_testSession == null)
                    _testSession = CreateNewTestSession();

                return _testSession;
            }
        }

        /// <summary>
        /// Gets the currently active Task
        /// </summary>
        public Task ActiveTask
        {
            get { return _testSession.ActiveTask; }
        }

        /// <summary>
        /// Gets the current browser
        /// </summary>
        public TBrowser Browser
        {
            get { return (TBrowser) (IWebBrowser) _testSession.Browser; }
        }

        /// <summary>
        /// Creates and starts a new test shell based on the parent task (useful for parallel testing)
        /// </summary>
        /// <returns>a new test shell</returns>
        public abstract ITestShell CreateNewTestShell(string testName);

        /// <summary>
        /// Creates a new test session instance
        /// </summary>
        /// <returns>A new test session</returns>
        protected virtual TestSession CreateNewTestSession()
        {
            return new TestSession(TestName);
        }

        /// <summary>
        /// Performs a default Sleep operation based on the Test shell's delay
        /// </summary>
        public void Sleep()
        {
            if (PageDelay > 0)
                Browser.Sleep(PageDelay);
        }

        /// <summary>
        /// Restarts the browser
        /// </summary>
        public void Restart()
        {
            Start(null);
        }

        private void LogRevisionInfo(Task task)
        {
            if (task == null)
                return;
            
        }

        /// <summary>
        /// Begins the test - sets up the test session or other items before the test executes
        /// </summary>
        public virtual void Start()
        {
            Start(null);
        }

        /// <summary>
        /// Begins a test using a specific parent task (allows for multi-threaded tests)
        /// </summary>
        /// <param name="parentTask">the parent task</param>
        public virtual void Start(Task parentTask)
        {
            if (TestSession.RootTask == null)
            {
                TestSession.StartLogger(parentTask);

                if (parentTask == null)
                {
                    // add the factories
                    AddLoggerFactories();
                }
            }
            else
            {
                if (parentTask != null)
                    throw new TestShellException(this, LogStatus.Fail, "SAJTestShell.Start cannot restart with a specified parent task.");

                Stop();
            }

            var active = ActiveTask;
            var task = active.BeginTask("TestShell.Start");
            try
            {
                LogRevisionInfo(task);
                for (var trycounter = 0; ; trycounter++)
                {
                    try
                    {
                        PreStartTestSession(_testSession);

                        _testSession.StartBrowser(GetStartupUrl());

                        PostStartTestSession(_testSession);

                        break;
                    }
                    catch (Exception e)
                    {
                        if (trycounter > 5)
                            throw;

                        if (!HandleStartException(task, e, trycounter))
                            throw;

                        task.AddLog(LogStatus.Information, "Exception was handled, trying again.");
                    }
                }

                task.Finish();
            }
            catch (Exception e)
            {
                Stop();
                task.Finish(e);
                throw;
            }
        }

        /// <summary>
        /// Handle an exception during the Start() method.
        /// </summary>
        /// <param name="task">The task used during the start method</param>
        /// <param name="e">The exception</param>
        /// <param name="tryCounter">The number of retries so far</param>
        protected virtual bool HandleStartException(Task task, Exception e, int tryCounter)
        {
            return false;
        }

        protected virtual void PostStartTestSession(TestSession session)
        {
        }

        protected virtual void PreStartTestSession(TestSession session)
        {
        }

        /// <summary>
        /// Stops the test
        /// </summary>
        public virtual void Stop()
        {
            if (_testSession != null)
                _testSession.Stop();
        }

        /// <summary>
        /// Encodes strText as plain-text so it is not treated as a regular expression
        /// this is useful when using descriptive programming and the strText may contain the special regexp characters
        /// Special regexp characters include: \ ^ $ * + ? . ( ) | { } [ ]
        /// </summary>
        /// <param name="strText">Text to encode</param>
        /// <returns>RegEx encoded string without any special regex processing.</returns>
        public static string EscapeTextForRegEx(string strText)
        {
            return Regex.Escape(strText);
        }

        /// <summary>
        /// Copies a file to a new unique filename in temp folder
        /// </summary>
        /// <param name="originalfile">the original filename</param>
        /// <returns>the new filename</returns>
        public static string CopyToUniqueFile(string originalfile)
        {
            string newfile;

            for (;;)
            {
                newfile = System.IO.Path.GetDirectoryName(originalfile) + "\\" + 
                                          System.IO.Path.GetFileNameWithoutExtension(originalfile) + "_" +
                                          RandomStuff.RandomWord(6) +
                                          System.IO.Path.GetExtension(originalfile);

                if (!System.IO.File.Exists(newfile))
                    break;
            }
            
            System.IO.File.Copy(originalfile, newfile);

            return newfile;
        }

        /// <summary>
        /// Deletes a file, but doesn't throw an exceptions if the file can't be deleted
        /// </summary>
        /// <param name="filename">the file to delete</param>
        public void DeleteFile(string filename)
        {
            if (string.IsNullOrEmpty(filename))
                return;

            try
            {
                var attr = System.IO.File.GetAttributes(filename);
                if ((attr & System.IO.FileAttributes.ReadOnly) == System.IO.FileAttributes.ReadOnly)
                {
                    attr &= ~System.IO.FileAttributes.ReadOnly;
                    System.IO.File.SetAttributes(filename, attr);
                }

                System.IO.File.Delete(filename);
            }
            catch (Exception)
            {
                ActiveTask.AddLog(LogStatus.Warning, "Unable to delete file {0}", filename);
            }
        }

    	/// <summary>
        /// Selects a random item from a WebList
        /// </summary>
        /// <param name="listbox">The WebList object</param>
        /// <param name="selectDifferentValue">If the value should be different than the currently selected value</param>
        /// <returns>The item that was selected</returns>
        public string SelectRandomItem(IWebControlWithItems listbox, bool selectDifferentValue = false, bool avoidDefaultValue = false)
        {
            var task = ActiveTask.BeginTask("SelectRandomItem");
            task.AddAttribute("Item", listbox.Selector);
            try
            {
                var items = listbox.Labels.Select((s, i) => new Tuple<string, int>(s, i)).ToList();

                if (avoidDefaultValue)
                {
                    items = items.Where(l => l.Item1 != "-- Select One --").ToList();
                }

                if (selectDifferentValue)
                {
                    var currentlySelected = listbox.Label;
                    items = items
                        .Where(l => l.Item1 != currentlySelected)
                        .ToList();

                    if (items.Count == 0)
                        throw new Exception("Unable to select a different item from [" + currentlySelected + " no more items available to select");
                }

                // loop through the labels to make sure we don't select one of the "no-nos"
                string label = null;

                while (items.Count > 0)
                {
                    var alabel = items.RandomItem();

                    if (CanSelectItem(alabel.Item1))
                    {
                        listbox.SelectedIndex = alabel.Item2;
                        label = alabel.Item1;
                        break;
                    }

                    items.Remove(alabel);
                }

                if (label == null)
                    throw new TestShellException(this, LogStatus.Fail, "Unable to find an item to select for " + listbox.Selector);

                task.Finish(label);

                return label;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

		/// <summary>
        /// Selects an item from a WebList.  If the item is null, then a random item is selected instead
        /// </summary>
        /// <param name="listbox">The WebList object</param> 
        /// <param name="label">The value it should select</param>
        /// <param name="selectDifferentValue">If the value should be different than the currently selected value</param>
        /// <returns>The item that was selected</returns>
        public string SelectItemOrRandom(IWebControlWithItems listbox, string label, bool selectDifferentValue = false, bool avoidDefaultValue = false)
		{
			if (label == null || selectDifferentValue)
                return SelectRandomItem(listbox, selectDifferentValue);

            listbox.Label = label;
            return listbox.Label;
        }

        /// <summary>
        /// Simple navigation to a non-SAJ site: www.google.com
        /// </summary>
        public void OffsiteNav()
        {
            OffsiteNav("http://www.google.com");
        }

        /// <summary>
        /// Navigates to a non-SAJ site
        /// </summary>
        /// <param name="url">the website for navigation</param>
        public void OffsiteNav(string url)
        {
            Browser.Navigate(url);
        }

        /// <summary>
        /// Returns the default startup URL
        /// </summary>
        /// <returns>A url to startup with</returns>
        public abstract string GetStartupUrl();

        /// <summary>
        /// Parses an integer from a string even when there are non-digit characters surrounding the digits.  eg: Found 98 jobs! will return 98
        /// </summary>
        /// <param name="str">the string to parse: </param>
        /// <returns>the integer value found</returns>
        public int ParseIntFromString(string str)
        {
            return str.ParseInt();
        }

        /// <summary>
        /// Parses a double/float from a string even when there are non-digit characters surrounding the digits.  eg: Found $135.20 jobs! will return 135.20
        /// </summary>
        /// <param name="str">the string to parse</param>
        /// <returns>the double value found</returns>
        public double ParseDoubleFromString(string str)
        {
            return str.ParseDouble();
        }

        /// <summary>
        /// Disposes the test shell by stopping it
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!_disposed)
                {
                    _disposed = true;
                    Interlocked.Decrement(ref _testsInProgress);

                    Stop();
                    if (_testSession != null)
                    {
                        _testSession.Dispose();
                        _testSession = null;
                    }
                }
            }
        }

        /// <summary>
        /// Gets a value from the static cache
        /// </summary>
        /// <param name="key">the unique key to the value</param>
        /// <returns>The value, or null</returns>
        public object GetCachedValue(object key)
        {
            lock (ValueHash)
            {
                return ValueHash.ContainsKey(key) ? ValueHash[key] : null;
            }
        }

        /// <summary>
        /// Sets a value in the static cache
        /// </summary>
        /// <param name="key">the unique key to the value</param>
        /// <param name="value">the value to set</param>
        /// <returns>the previously cached value</returns>
        public object SetCachedValue(object key, object value)
        {
            lock (ValueHash)
            {
                var result = GetCachedValue(key);
                ValueHash[key] = value;

                return result;
            }
        }

        /// <summary>
        /// Add the logger factories to the test shell
        /// </summary>
        protected abstract void AddLoggerFactories();

        /// <summary>
        /// Determines whether this instance can select an item of the specified label value from any list
        /// </summary>
        /// <param name="label">The label.</param>
        /// <returns>
        /// 	<c>true</c> if this instance [can select random item] the specified label; otherwise, <c>false</c>.
        /// </returns>
        protected virtual bool CanSelectItem(string label)
        {
            return true;
        }

        /// <summary>
        /// An exception for the SAJTestShell object
        /// </summary>
        public class TestShellException : LogStatusException
        {
            /// <summary>
            /// Initializes a new instance of the SAJTestShellException class.
            /// </summary>
            /// <param name="shell">The test shell</param>
            /// <param name="status">the status to log</param>
            /// <param name="message">The message</param>
            public TestShellException(TestShell<TBrowser, TPage> shell, LogStatus status, string message)
                : base(status, message)
            {
                Shell = shell;
            }

            /// <summary>
            /// Gets the shell for the exception
            /// </summary>
            public TestShell<TBrowser, TPage> Shell { get; private set; }
        }
    }
}
